LLVM IR 中的异常处理与平台支持密切相关，将使用libunwind来了解最常见的异常处理类型。C++可以充分发挥libunwind的潜力，先来看一个C++示例，其中bar()函数可以抛出一个int或double值：

\begin{cpp}
int bar(int x) {
    if (x == 1) throw 1;
    if (x == 2) throw 42.0;
    return x;
}
\end{cpp}

foo()函数调用bar()，但只处理抛出的int值，并且还声明只抛出int值：

\begin{cpp}
int foo(int x) {
    int y = 0;
    try {
        y = bar(x);
    }
    catch (int e) {
        y = e;
    }
    return y;
}
\end{cpp}

抛出异常需要两次调用运行时库，可以在bar()函数中看到。\_\_cxa\_allocate\_exception()为异常分配内存，这个函数接受要分配的字节数作为参数。异常负载(本例中的int或double值)被复制到分配的内存中，再通过\_\_cxa\_throw()引发异常。这个函数接受三个参数:指向已分配异常的指针、有关有效负载的类型信息，以及指向析构函数的指针(若异常有有效负载有的话)。\_\_cxa\_throw()函数启动堆栈展开过程，并且永远不会返回。LLVM IR中，对int值执行:

\begin{shell}
%eh = call ptr @__cxa_allocate_exception(i64 4)
store i32 1, ptr %eh
call void @__cxa_throw(ptr %eh, ptr @_ZTIi, ptr null)
unreachable
\end{shell}

\_ZTIi是描述int类型的类型信息。对于double类型，则为\_ZTId。

目前，还没有做特定于LLVM的工作。这一点在foo()函数中发生了变化，对bar()的调用可能引发异常。若是int类型异常，控制流必须转移到catch的IR代码。所以，必须使用invoke指令，而非call指令:

\begin{shell}
%y = invoke i32 @_Z3bari(i32 %x) to label %next
                                 unwind label %lpad
\end{shell}

这两条指令的区别在于，invoke有两个相关联的标签。在函数正常结束时，第一个标号表示执行继续，通常以ret指令结束。示例代码中，这个标签名为\%next。若发生异常，则在标号为\%lpad的“着陆垫”上继续执行。

著陆台是一个基本的块，必须以landingpad指令开始。landingpad指令向LLVM提供有关处理过的异常类型的信息。例如，一个着陆台可能是这样的:

\begin{shell}
lpad:
%exc = landingpad { ptr, i32 }
            cleanup
            catch ptr @_ZTIi
            filter [1 x ptr] [ptr @_ZTIi]
\end{shell}

这里有三种可能的操作类型：

\begin{itemize}
\item
cleanup:表示存在用于清理当前状态的代码，用于调用局部对象的析构函数。若此标记存在，则在堆栈展开期间调用着陆垫指令。

\item
catch:是类型-值对的列表，表示可以处理的异常类型。若在此列表中找到抛出的异常类型，则调用着陆垫指令。在foo()函数的情况下，该值是指向int类型的C++运行时类型信息的指针，类似于\_\_cxa\_throw()函数的参数。

\item
filter:指定一个异常类型数组。若在数组中找不到当前异常的异常类型，则调用着陆垫指令，用于实现throw()规范。对于foo()函数，数组只有一个成员——int类型的类型信息。
\end{itemize}

着陆平垫指令的结果类型是\{ptr, i32 \}结构。第一个元素是指向抛出异常的指针，第二个元素是类型选择器。从结构体中进行提取:

\begin{shell}
%exc.ptr = extractvalue { ptr, i32 } %exc, 0
%exc.sel = extractvalue { ptr, i32 } %exc, 1
\end{shell}

类型选择器是一个数字，可以帮助确定为什么要调用着陆垫指令。若当前异常类型与landingpad指令的catch部分给出的异常类型匹配，则该值为正值。若当前异常类型不匹配过滤部分给出的值，则值为负。若应该调用清理代码，则为0。

类型选择器是类型信息表的偏移量，由着陆垫指令的catch和filter部分给出的值构造而成。优化过程中，多个着陆垫指令可以合并为一个，所以在IR级别上该表的结构是未知的。要检索给定类型的类型选择器，需要调用内部的@llvm.eh.typeid.for的函数。需要它来检查类型选择器值是否与int的类型信息相对应，以便执行catch (int e) \{\}块中的代码:

\begin{shell}
%tid.int = call i32 @llvm.eh.typeid.for(ptr @_ZTIi)
%tst.int = icmp eq i32 %exc.sel, %tid.int
br i1 %tst.int, label %catchint, label %filterorcleanup
\end{shell}

异常的处理是通过调用\_\_cxa\_begin\_catch()和\_\_cxa\_end\_catch()实现的。\_\_cxa\_begin\_catch()函数需要一个参数——当前异常——是着陆垫指令返回的值之一，返回一个指向异常负载的指针——本例中是int值。

\_\_cxa\_end\_catch()函数标志着异常处理的结束，并释放用\_\_cxa\_allocate\_exception()分配的内存。注意，若在catch块中抛出另一个异常，则运行时行为会复杂得多。异常处理方法如下:

\begin{shell}
catchint:
%payload = call ptr @__cxa_begin_catch(ptr %exc.ptr)
%retval = load i32, ptr %payload
call void @__cxa_end_catch()
br label %return
\end{shell}

若当前异常的类型与throws()声明中的列表不匹配，则调用意外异常处理程序，需要再次检查类型选择器:

\begin{shell}
filterorcleanup:
%tst.blzero = icmp slt i32 %exc.sel, 0
br i1 %tst.blzero, label %filter, label %cleanup
\end{shell}

若类型选择器的值小于0，则调用该处理程序:

\begin{shell}
filter:
call void @__cxa_call_unexpected(ptr %exc.ptr) #4
unreachable
\end{shell}

同样，该处理程序也不会返回。

这种情况下不需要清理，所有清理代码要做的就是恢复堆栈展开的执行:

\begin{shell}
cleanup:
resume { ptr, i32 } %exc
\end{shell}

libunwind驱动堆栈展开过程，但没有绑定到语言，依赖于语言的处理在个性函数中完成。对于Linux上的C++，个性函数(personality function)称为\_\_gxx\_personality\_v0()。根据平台或编译器的不同，这个名称可能有所不同。每个需要参与堆栈展开的函数都有一个个性函数。此个性函数分析函数是否捕获异常、是否具有不匹配的筛选器列表或是否需要清理调用，将这些信息返回给解卷器，解卷器采取相应的行动。在LLVM IR中，个性函数的指针是作为函数定义的一部分给出的:

\begin{shell}
define i32 @_Z3fooi(i32) personality ptr @__gxx_personality_v0
\end{shell}

至此，异常处理工具就完成了。

要在编译器中为编程语言使用异常处理，最简单的策略是利用现有的C++运行时函数。这还有一个好处，就是异常可以与C++互操作。缺点是将一些C++运行时绑定到语言的运行时中，尤其是内存管理。若想避免这种情况，则需要创建自己的\_cxa\_函数。不过，想必还是会想要使用libunwind，因为它提供了堆栈解旋机制:

\begin{enumerate}
\item
来看看如何创建这个IR，在第2章中创建了calc表达式编译器。将扩展表达式编译器的代码生成器，以便在执行除零操作时引发并处理异常。生成的IR将检查除法的除数是否为0。若为true，则会引发异常。还将在函数中添加一个着陆垫指令，用来捕获异常并打印Divide by 0 !到控制台，并结束计算。这个简单的例子中，不需要使用异常处理，但可以让专注于代码生成过程。必须将所有代码添加到CodeGen.cpp文件中，首先添加所需的新字段和一些辅助方法，需要存储\_\_cxa\_allocate\_exception()和\_\_cxa\_throw()函数的LLVM声明，其由函数类型和函数本身组成。需要一个GlobalVariable实例来保存类型信息，还需要引用控制着陆垫指令的基本块和包含不可达指令的基本块:

\begin{cpp}
GlobalVariable *TypeInfo = nullptr;
FunctionType *AllocEHFty = nullptr;
Function *AllocEHFn = nullptr;
FunctionType *ThrowEHFty = nullptr;
Function *ThrowEHFn = nullptr;
BasicBlock *LPadBB = nullptr;
BasicBlock *UnreachableBB = nullptr;
\end{cpp}

\item
还要添加一个新辅助函数，来比较两个值的IR。createICmpEq()函数接受Left和Right值作为参数进行比较，创建一个比较指令来测试值是否相等，并为两个基本块创建一个分支指令，用于相等和不相等的情况，这两个基本块通过TrueDest和FalseDest参数中的引用返回。此外，新基本块的标签可以在TrueLabel和FalseLabel参数中给出:

\begin{cpp}
void createICmpEq(Value *Left, Value *Right,
                    BasicBlock *&TrueDest,
                    BasicBlock *&FalseDest,
                    const Twine &TrueLabel = "",
                    const Twine &FalseLabel = "") {
    Function *Fn =
        Builder.GetInsertBlock()->getParent();
    TrueDest = BasicBlock::Create(M->getContext(),
                                    TrueLabel, Fn);
    FalseDest = BasicBlock::Create(M->getContext(),
                                    FalseLabel, Fn);
    Value *Cmp = Builder.CreateCmp(CmpInst::ICMP_EQ,
                                    Left, Right);
    Builder.CreateCondBr(Cmp, TrueDest, FalseDest);
}
\end{cpp}

\item
要使用运行时中的函数，需要创建几个函数声明。在LLVM中，函数类型给出了签名，并且必须构造函数本身，使用createFunc()方法创建这两个对象。函数需要引用FunctionType和Function指针、新声明函数的名称和结果类型。参数类型列表是可选的，变量参数列表标志设置为false，表示参数列表中没有变量部分:

\begin{cpp}
void createFunc(FunctionType *&Fty, Function *&Fn,
                const Twine &N, Type *Result,
                ArrayRef<Type *> Params = None,
                bool IsVarArgs = false) {
    Fty = FunctionType::get(Result, Params, IsVarArgs);
    Fn = Function::Create(
        Fty, GlobalValue::ExternalLinkage, N, M);
}
\end{cpp}
\end{enumerate}

完成这些准备工作后，就可以生成抛出异常的IR了。

\mySubsubsection{6.1.1.}{抛出异常}

为了生成引发异常的IR代码，将添加addThrow()方法。这个新方法需要初始化新字段，然后通过\_\_cxa\_throw()函数生成引发异常的IR。所引发异常的有效负载为int类型，可以设置为任意值。下面是需要编写的代码:

\begin{enumerate}
\item
新的addThrow()方法首先检查TypeInfo字段是否已初始化。若还没有初始化，则会创建一个i8指针类型的全局外部常量\_ZTIi。这代表了描述C++ int类型的元数据:

\begin{cpp}
void addThrow(int PayloadVal) {
    if (!TypeInfo) {
        TypeInfo = new GlobalVariable(
        *M, Int8PtrTy,
        /*isConstant=*/true,
        GlobalValue::ExternalLinkage,
        /*Initializer=*/nullptr, "_ZTIi");
\end{cpp}

\item
初始化继续使用辅助createFunc()方法为\_\_cxa\_allocate\_exception()和\_\_cxa\_throw()函数创建IR声明:

\begin{cpp}
        createFunc(AllocEHFty, AllocEHFn,
                "__cxa_allocate_exception", Int8PtrTy,
                {Int64Ty});
        createFunc(ThrowEHFty, ThrowEHFn, "__cxa_throw",
                VoidTy,
                {Int8PtrTy, Int8PtrTy, Int8PtrTy});
\end{cpp}

\item
使用异常处理的函数需要一个个性函数，有助于堆栈展开。添加了IR代码来声明C++库中的\_\_gxx\_personality\_v0()个性函数，并将其设置为当前函数的个性例程。当前函数没有存储为一个字段，可以使用Builder实例来查询当前的基本块，其将函数存储为Parent字段:

\begin{cpp}
        FunctionType *PersFty;
        Function *PersFn;
        createFunc(PersFty, PersFn,
                    "__gxx_personality_v0", Int32Ty, std::nulopt,
                    true);
        Function *Fn =
        Builder.GetInsertBlock()->getParent();
        Fn->setPersonalityFn(PersFn);
\end{cpp}

\item
接下来，必须创建并填充着陆台指令的基本块。首先，需要保存指向当前基本块的指针，再创建一个新的基本块，在构建器中进行设置，以便可以用作插入指令的基本块，并调用addLandingPad()方法。该方法生成用于处理异常的IR代码，将在下一节捕获异常中进行描述。这段代码填充了着陆垫指令的基本块:

\begin{cpp}
        BasicBlock *SaveBB = Builder.GetInsertBlock();
        LPadBB = BasicBlock::Create(M->getContext(),
                                    "lpad", Fn);
        Builder.SetInsertPoint(LPadBB);
        addLandingPad();
\end{cpp}

\item
初始化部分通过创建保存不可达指令的基本块来完成，并创建基本块并将其设置为构建器中的插入点。然后，可以将不可达指令添加到其中，再将构建器的插入点设置回已保存的SaveBB实例，以便将以下IR添加到正确的基本块中:

\begin{cpp}
        UnreachableBB = BasicBlock::Create(
        M->getContext(), "unreachable", Fn);
        Builder.SetInsertPoint(UnreachableBB);
        Builder.CreateUnreachable();
        Builder.SetInsertPoint(SaveBB);
    }
\end{cpp}

\item
要抛出异常，通过调用\_\_cxa\_allocate\_exception()函数为异常和负载分配内存。我们的有效负载是C++ int类型，通常大小为4字节，再为size创建一个常量无符号值，并将其作为参数调用函数。函数类型和函数声明已经初始化，所以只需要创建调用指令:

\begin{cpp}
    Constant *PayloadSz =
        ConstantInt::get(Int64Ty, 4, false);
    CallInst *EH = Builder.CreateCall(
        AllocEHFty, AllocEHFn, {PayloadSz});
\end{cpp}

\item
接下来，将PayloadVal值存储在分配的内存中，需要通过调用ConstantInt::get()函数创建一个LLVM IR常量。所述指向所分配内存的指针为i8指针类型;为了存储i32类型的值，需要创建一个bitcast指令来强制转换该类型:

\begin{cpp}
    Value *PayloadPtr =
        Builder.CreateBitCast(EH, Int32PtrTy);
    Builder.CreateStore(
        ConstantInt::get(Int32Ty, PayloadVal, true),
        PayloadPtr);
\end{cpp}

\item
最后，必须通过调用\_\_cxa\_throw()函数来抛出异常。由于这个函数引发了一个异常，该异常也在同一个函数中处理，所以需要使用invoke指令，而非call指令。与调用指令不同，因为它有两个后继基本块，所以调用指令会结束一个基本块，这些是UnreachableBB和LPadBB基本块。若函数没有引发异常，控制流将转移到UnreachableBB基本块。由于\_\_cxa\_throw()函数的设计，因为控制流会转移到LPadBB基本块来处理异常，所以这种情况永远不会发生。这样就完成了addThrow()方法的实现:

\begin{cpp}
    Builder.CreateInvoke(
    ThrowEHFty, ThrowEHFn, UnreachableBB, LPadBB,
    {EH,
     ConstantExpr::getBitCast(TypeInfo, Int8PtrTy),
     ConstantPointerNull::get(Int8PtrTy)});
}
\end{cpp}

\end{enumerate}

接下来，将添加代码来生成处理异常的IR。

\mySubsubsection{6.1.2.}{捕捉异常}

要生成捕获异常的IR代码，必须添加addLandingPad()方法。生成的IR从异常中提取类型信息。若匹配C++ int类型，则通过打印Divide by 0!到控制台来处理异常并从函数中返回。若类型不匹配，只需执行resume指令，该指令将控制转移回运行时。由于调用层次结构中没有其他函数来处理此异常，因此将终止应用程序的运行。以下步骤描述了生成用于捕获异常的IR:

\begin{enumerate}
\item
生成的IR中，需要从C++运行时库中调用\_\_cxa\_begin\_catch()和\_\_cxa\_end\_catch()函数。为了输出错误消息，将从C运行时库生成对puts()函数的调用。此外，为了从异常中获取类型信息，必须生成对llvm.eh.typeid.for指令的调用。还需要FunctionType和Function实例，我们将利用createFunc()方法来创建它们:

\begin{cpp}
void addLandingPad() {
    FunctionType *TypeIdFty; Function *TypeIdFn;
    createFunc(TypeIdFty, TypeIdFn,
                "llvm.eh.typeid.for", Int32Ty,
                {Int8PtrTy});
    FunctionType *BeginCatchFty; Function *BeginCatchFn;
    createFunc(BeginCatchFty, BeginCatchFn,
                "__cxa_begin_catch", Int8PtrTy,
                {Int8PtrTy});
    FunctionType *EndCatchFty; Function *EndCatchFn;
    createFunc(EndCatchFty, EndCatchFn,
                "__cxa_end_catch", VoidTy);
    FunctionType *PutsFty; Function *PutsFn;
    createFunc(PutsFty, PutsFn, "puts", Int32Ty,
                {Int8PtrTy});
\end{cpp}

\item
着陆台指令是我们生成的第一条指令，结果类型是一个包含i8指针和i32类型字段的结构体。这个结构是通过调用StructType::get()函数生成的。此外，由于需要处理C++ int类型的异常，还需要将this作为子句添加到landingpad指令中，该指令必须是i8指针类型的常量，所以需要生成一个位转换指令来将TypeInfo值转换为这种类型，再将指令返回的值存储在Exc变量中，供后续使用:

\begin{cpp}
    LandingPadInst *Exc = Builder.CreateLandingPad(
        StructType::get(Int8PtrTy, Int32Ty), 1, "exc");
    Exc->addClause(
        ConstantExpr::getBitCast(TypeInfo, Int8PtrTy));
\end{cpp}

\item
接下来，从返回值中提取类型选择器。调用llvm.eh.typeid.for指令，检索TypeInfo字段的类型ID，表示C++ int类型。使用这个IR，已经生成了两个值。我们需要进行比较，以决定是否可以处理异常:

\begin{cpp}
    Value *Sel =
        Builder.CreateExtractValue(Exc, {1}, "exc.sel");
    CallInst *Id =
        Builder.CreateCall(TypeIdFty, TypeIdFn,
                            {ConstantExpr::getBitCast(
                                TypeInfo, Int8PtrTy)});
\end{cpp}

\item
要生成用于比较的IR，必须调用createICmpEq()函数。这个函数还生成了两个基本块，将它们存储在TrueDest和FalseDest变量中:

\begin{cpp}
    BasicBlock *TrueDest, *FalseDest;
    createICmpEq(Sel, Id, TrueDest, FalseDest, "match",
                "resume");
\end{cpp}

\item
若这两个值不匹配，控制流将在false基本块处继续。这个基本块只包含一个resume指令，将控制权交还给C++运行时:

\begin{cpp}
    Builder.SetInsertPoint(FalseDest);
    Builder.CreateResume(Exc);
\end{cpp}

\item
若这两个值相等，则控制流在TrueDest基本块处继续。生成IR代码，以便从存储在Exc变量中的着陆平台指令的返回值中，提取指向异常的指针。生成对\_\_cxa\_begin\_catch()函数的调用，将指向异常的指针作为参数传递。这表示在运行时开始处理异常:

\begin{cpp}
    Builder.SetInsertPoint(TrueDest);
    Value *Ptr =
        Builder.CreateExtractValue(Exc, {0}, "exc.ptr");
    Builder.CreateCall(BeginCatchFty, BeginCatchFn,
                        {Ptr});
\end{cpp}

\item
再用puts()函数来处理异常，将消息输出到控制台，这里通过调用CreateGlobalStringPtr()函数生成一个指向该字符串的指针，在生成的调用中将该指针作为参数传递给puts()函数:

\begin{cpp}
    Value *MsgPtr = Builder.CreateGlobalStringPtr(
        "Divide by zero!", "msg", 0, M);
    Builder.CreateCall(PutsFty, PutsFn, {MsgPtr});
\end{cpp}

\item
现在已经处理了异常，必须生成对\_\_cxa\_end\_catch()函数的调用，以通知运行时有关它的信息。最后，从函数返回一个aret指令:

\begin{cpp}
    Builder.CreateCall(EndCatchFty, EndCatchFn);
    Builder.CreateRet(Int32Zero);
}
\end{cpp}

\end{enumerate}

使用addThrow()和addLandingPad()函数，可以生成引发异常和处理异常的IR，但仍需要添加IR来检查除数是否为0。

我们将在下一节中讨论这一点。

\mySubsubsection{6.1.3.}{将异常处理代码集成到应用程序中}

除法的IR是在visit(BinaryOp \&)方法中生成的，必须生成一个IR来将除数与0进行比较，而不是仅仅生成一个sdiv指令。若除数为0，则控制流继续在基本块中运行，从而引发异常；否则，控制流将在带有sdiv指令的基本块中继续。在createICmpEq()和addThrow()函数的帮助下，可以编写如下代码:

\begin{cpp}
    case BinaryOp::Div:
        BasicBlock *TrueDest, *FalseDest;
        createICmpEq(Right, Int32Zero, TrueDest,
                     FalseDest, "divbyzero", "notzero");
        Builder.SetInsertPoint(TrueDest);
        addThrow(42); // Arbitrary payload value.
        Builder.SetInsertPoint(FalseDest);
        V = Builder.CreateSDiv(Left, Right);
        break;
\end{cpp}

代码生成部分现在已经完成。要构建应用程序，必须切换到构建目录并运行ninja工具:

\begin{shell}
$ ninja
\end{shell}

构建完成后，可以用with a: 3/a表达式检查生成的IR:

\begin{shell}
$ src/calc "with a: 3/a"
\end{shell}

将看到抛出和捕获异常所需的IR。

生成的IR现在依赖于C++运行时，链接所需库的最简单方法是使用clang++编译器。使用表达式计算器的运行时函数将rtcalc.c文件重命名为rtcalc.cpp，并在文件中的每个函数前面添加extern "C"。然后，使用llc工具将生成的IR转换为目标文件，并使用clang++编译器创建可执行文件:

\begin{shell}
$ src/calc "with a: 3/a" | llc -filetype obj -o exp.o
$ clang++ -o exp exp.o ../rtcalc.cpp
\end{shell}

现在，可以用不同的值对程序进行测试:

\begin{shell}
$ ./exp
Enter a value for a: 1
The result is: 3
$ ./exp
Enter a value for a: 0
Divide by zero!
\end{shell}

第二次运行时，输入为0，这会引发异常——像预期的那样工作了!

本节中，了解了如何引发和捕获异常。生成IR的代码可以用作其他编译器的蓝图，所使用的类型信息和catch子句的数量取决于编译器的输入，但需要生成的IR仍然遵循本节中提供的模式。

添加元数据是向LLVM提供进一步信息的另一种方式。下一节中，将添加类型元数据，以便在某些情况下支持LLVM优化器。









































